package com.server.questionbank.snframe.service.OllamaService.KnowledgeBaseService;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.server.questionbank.snframe.config.ollama.KnowledgeBaseConfig;
import com.server.questionbank.snframe.service.OllamaService.KnowledgeBaseService.TextEmbeddingService.TextEmbeddingService;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class KnowledgeBaseServiceImpl implements KnowledgeBaseService {
    private static final Logger logger = LoggerFactory.getLogger(KnowledgeBaseServiceImpl.class);

    @Autowired // 注入Ollama配置
    private KnowledgeBaseConfig knowledgeBaseConfig;

    @Autowired // 注入文本向量化服务类
    private TextEmbeddingService textEmbeddingService;

    @Autowired // 注入数据库
    private KnowledgeDataBase knowledgeDataBase;

    // 系统提示
    private static List<String> SystemMessage = new ArrayList<>();

    @Autowired // 注入redis模板类
    private RedisTemplate<String, Object> redisTemplate;

    // 知识库内容常量
    private static final String KB_CONTENT_DIR = "knowledge_base_content_";
    // 知识库向量常量
    private static final String KB_EMBEDDING_DIR = "knowledge_base_embeddings";
    // 知识库更新时间常量
    private static final String KB_UPDATE_TIME = "knowledge_base_update_time";

    // 初始化知识库
    public Mono<Boolean> initialize() {
        // 添加基本提示
        logger.info("🔄 初始化 AI认知...");
        SystemMessage.add("你是一个AI助手，你的名字叫小云。你需要结合与分析给定的知识库进行回答。如果有数据库数据，你需要分析后在给出回答。你的回答要基于知识库中的数据。");

        // 每次启动时，检查是否需要更新知识库
        executeDailyTask();

        // 初始化完成
        logger.info("✅ 知识库 初始化完成");
        return Mono.just(true);
    }
    // 获取系统提示
    @Override
    public List<String> getSystemMessage() {
        return SystemMessage;
    }

    // 获取文件知识库
    @Override
    public String getFliesKnowledgeBase(String query) {
        StringBuilder knowledgeBase = new StringBuilder();
        Map<SourceType, List<KnowledgeItem>> knowledgeBasesList = getKnowledgeBasesList(query);
        knowledgeBase.append("[文档库内容]：\n");
        if (knowledgeBasesList.get(SourceType.File).size() > 0)
        {
            knowledgeBasesList.get(SourceType.File).forEach(knowledgeItem -> {
                knowledgeBase.append(knowledgeItem.getContent());
                knowledgeBase.append("\n");
            });
        } else knowledgeBase.append("没有找到任何内容！\n");
        return knowledgeBase.toString();
    }

    // 获取数据库知识库
    @Override
    public String getDatabaseKnowledgeBase(String query) {
        StringBuilder knowledgeBase = new StringBuilder();
        Map<SourceType, List<KnowledgeItem>> knowledgeBasesList = getKnowledgeBasesList(query);
        knowledgeBase.append("[数据库表内容]：\n");
        if (knowledgeBasesList.get(SourceType.Table).size() > 0)
        {
            knowledgeBasesList.get(SourceType.Table).forEach(knowledgeItem -> {
                knowledgeBase.append(knowledgeItem.getContent());
                knowledgeBase.append("\n");
            });
        } else knowledgeBase.append("没有找到任何内容！\n");
        return knowledgeBase.toString();
    }

    // 获取所有知识库
    @Override
    public String getKnowledgeBases(String query) {
        StringBuilder knowledgeBase = new StringBuilder();
        knowledgeBase.append("以下是知识库内容：\n");
        Map<SourceType, List<KnowledgeItem>> knowledgeBasesList = getKnowledgeBasesList(query);
        knowledgeBase.append("[文档库内容]：\n");
        if (knowledgeBasesList.get(SourceType.File).size() > 0)
        {
            knowledgeBasesList.get(SourceType.File).forEach(knowledgeItem -> {
                knowledgeBase.append(knowledgeItem.getContent());
                knowledgeBase.append("\n");
            });
        }
        else knowledgeBase.append("没有找到任何内容！\n");

        knowledgeBase.append("[数据库表内容]：\n");
        if (knowledgeBasesList.get(SourceType.Table).size() > 0)
        {
            knowledgeBasesList.get(SourceType.Table).forEach(knowledgeItem -> {
                knowledgeBase.append(knowledgeItem.getContent());
                knowledgeBase.append("\n");
            });
        }
        else knowledgeBase.append("没有找到任何内容！\n");
        return knowledgeBase.toString();
    }

    // 获取知识库列表
    private Map<SourceType, List<KnowledgeItem>> getKnowledgeBasesList(String query) {
        try {
            // 生成双路向量
            float[] exactVec = textEmbeddingService.generateEmbedding(query).block();

            // 从 Redis 获取所有嵌入向量
            Map<Object, Object> embeddingsMap = redisTemplate.opsForHash().entries(KB_EMBEDDING_DIR);

            // 创建一个列表来存储相似度
            List<KnowledgeItemSimilarity> similarities = new ArrayList<>();

            ObjectMapper objectMapper = new ObjectMapper();

            // 遍历嵌入向量，计算余弦相似度
            embeddingsMap.forEach((key, value) -> {
                try {
                    String chunkKey = (String) key;
                    if (value instanceof ArrayList) {
                        ArrayList<Double> doubleList = (ArrayList<Double>) value;
                        float[] docVec = new float[doubleList.size()];
                        for (int i = 0; i < doubleList.size(); i++) {
                            docVec[i] = doubleList.get(i).floatValue();  // 将 Double 转换为 float
                        }

                        // 计算余弦相似度
                        float exactScore = textEmbeddingService.cosineSimilarity(exactVec, docVec);

                        // 获取该嵌入向量对应的内容
                        LinkedHashMap contentMap = (LinkedHashMap) redisTemplate.opsForValue().get(KB_CONTENT_DIR + chunkKey);
                        KnowledgeItem content = objectMapper.convertValue(contentMap, KnowledgeItem.class);

                        // 将相似度和内容存入列表
                        similarities.add(new KnowledgeItemSimilarity(chunkKey, content, exactScore));
                    } else {
                        logger.error("无法转换类型: {}", value.getClass().getName());
                    }
                } catch (Exception e) {
                    logger.error("获取嵌入向量或内容失败: {}", e.getMessage());
                }
            });

            // 按相似度加权排序并返回结果
            Map<SourceType, List<KnowledgeItem>> result = weightedSorting(similarities, query);

            // 验证配置
            int topN = knowledgeBaseConfig.getGeDataTop();
            if (topN <= 0) {
                throw new IllegalArgumentException("topN 必须大于0，当前值: " + topN);
            }

            // 返回 前10条数据
            result.computeIfPresent(SourceType.File, (k, v) -> getTopNResults(v, knowledgeBaseConfig.getGeDataTop()));
            result.computeIfPresent(SourceType.Table, (k, v) -> getTopNResults(v, knowledgeBaseConfig.getGeDataTop()));

            logger.info("知识库查询结果 | query:{} | fileItems:{}, tableItems:{}",
                    query,
                    result.getOrDefault(SourceType.File, Collections.emptyList()).size(),
                    result.getOrDefault(SourceType.Table, Collections.emptyList()).size());
            return result;

        } catch (Exception e) {
            logger.error("知识库查询异常 | query:{} | error:{}",
                    query, ExceptionUtils.getStackTrace(e));
            return null;
        }
    }

    // 内部类，用于存储相似度和内容
    private static class KnowledgeItemSimilarity {
        private String key;
        private KnowledgeItem content;
        private float similarity;

        public KnowledgeItemSimilarity(String key, KnowledgeItem content, float similarity) {
            this.key = key;
            this.content = content;
            this.similarity = similarity;
        }

        public KnowledgeItem getContent() {
            return content;
        }

        public float getSimilarity() {
            return similarity;
        }
    }

    // 每天北京时间 00:00 执行 更新知识库
    @Scheduled(cron = "0 0 ${knowledgeBase.updateTime} * * ?", zone = "Asia/Shanghai")
    @Transactional(rollbackFor = Exception.class)
    public void executeDailyTask() {
        String lastUpdate = (String) redisTemplate.opsForValue().get(KB_UPDATE_TIME);
        if (lastUpdate != null && !lastUpdate.isEmpty() && knowledgeBaseConfig.getUpdateInterval() > 0){
            try {
                // 1. 解析 Redis 中的时间（带时区）
                DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
                LocalDateTime lastUpdateTime = LocalDateTime.parse(lastUpdate, formatter);

                // 2. 对齐到配置的每日时间点（如 03:00）
                ZonedDateTime zonedLastUpdate = lastUpdateTime.atZone(ZoneId.of("Asia/Shanghai"));
                ZonedDateTime alignedLastUpdate = alignToScheduledTime(zonedLastUpdate, knowledgeBaseConfig.getUpdateTime());

                // 3. 计算应更新时间点（对齐后的时间 + 间隔天数）
                ZonedDateTime nextUpdateTime = alignedLastUpdate.plusDays(knowledgeBaseConfig.getUpdateInterval());

                // 4. 获取当前时间（同一时区）
                ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

                // 5. 判断是否到期
                if (now.isBefore(nextUpdateTime)) {
                    // 5.1 计算剩余时间并记录
                    Duration remaining = Duration.between(now, nextUpdateTime);
                    long days = remaining.toDays();
                    long hours = remaining.toHours() % 24;
                    long minutes = remaining.toMinutes() % 60;
                    long seconds = remaining.getSeconds() % 60;

                    String remainingTime = String.format("%d天%d小时%d分钟%d秒钟", days, hours, minutes, seconds);
                    logger.warn("⏰ 知识库未达更新时间，剩余: {}", remainingTime);
                    return;
                }

                // 5.2 业务时间已到期，继续执行更新（即使 TTL 未过期）
                logger.info("✅ 知识库已到期，立即更新...");
            } catch (DateTimeParseException e) {
                logger.error("❌ Redis 时间格式错误: {}", lastUpdate, e);
                return;
            }
        }

        logger.info("🔄 开始知识库更新任务...");

        logger.info("🧹 清理历史数据...");
        cleanPreviousData();

        makeKnowledgeBase();
    }

    // 制作知识库
    private void makeKnowledgeBase() {

        // 进度状态对象
        class Progress {
            int totalItems;
            int processedItems;
            int totalChunks;
            int processedChunks;
            long startTime = System.currentTimeMillis();
        }
        Progress progress = new Progress();

        try {

            // 获取数据源
            List<KnowledgeItem> knowledgeItems = new ArrayList<>();
            knowledgeItems.addAll(safeReadFiles(knowledgeBaseConfig.getFilePath()));
            knowledgeItems.addAll(safeReadDatabase());
            progress.totalItems = knowledgeItems.size();

            if (knowledgeItems.isEmpty()) {
                logger.warn("⚠️ 未获取到有效知识数据");
                try {
                    // 存储更新时间
                    redisTemplate.opsForValue().set(
                            KB_UPDATE_TIME,
                            LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
                            knowledgeBaseConfig.getUpdateInterval(),
                            TimeUnit.DAYS
                    );
                } catch (Exception e) {
                    logger.error("存储更新时间失败: {}", ExceptionUtils.getStackTrace(e));
                }
                long elapsed = (System.currentTimeMillis() - progress.startTime) / 1000;
                logger.info("✅ 更新完成! 没有写入任何知识数据 | 总耗时: {}秒", elapsed);
                return;
            }

            // 3. 处理数据
            logger.info("⏳ 开始处理 {} 个知识项...", progress.totalItems);
            AtomicInteger successCount = new AtomicInteger();
            Map<String, float[]> embeddingBatch = new ConcurrentHashMap<>();
            Map<String, KnowledgeItem> contentBatch = new ConcurrentHashMap<>();

            // 并行处理数据
            knowledgeItems.parallelStream().forEach(item -> {
                // 更新进度
                int currentItem = progress.processedItems++;
                if (currentItem % 10 == 0) { // 每10项输出一次进度
                    logger.info("📊 进度: {}/{} ({}%) | 已生成块: {} | 耗时: {}s",
                            currentItem, progress.totalItems,
                            (currentItem * 100 / progress.totalItems),
                            progress.processedChunks,
                            (System.currentTimeMillis() - progress.startTime) / 1000);
                }

                try {
                    // 知识项分块
                    List<String> chunks = TextChunker.chunk(item.getContent(), knowledgeBaseConfig.getChunkSize());
                    progress.totalChunks += chunks.size();

                    // 生成向量并保存到缓存中
                    chunks.forEach(chunk -> {
                        try {
                            float[] embedding = textEmbeddingService.generateEmbedding(chunk)
                                    .block(Duration.ofSeconds(10));
                            System.out.println("知识库向量：[" + embedding.length + "]");
                            for (float f : embedding)
                                System.out.print(f + " ");
                            System.out.println();

                            String chunkKey = DigestUtils.md5Hex(chunk);
                            embeddingBatch.put(chunkKey, embedding);
                            contentBatch.put(chunkKey, new KnowledgeItem(item.getSourceType(), chunk));

                            progress.processedChunks++;
                            successCount.incrementAndGet();
                        } catch (Exception e) {
                            logger.error("向量生成失败 | source:{} | error:{}",
                                    item.getSourceType(), e);
                        }
                    });
                } catch (Exception e) {
                    logger.error("分块处理失败 | source:{} | error:{}",
                            item.getSourceType(), e);
                }
            });

            // 4. 保存结果
            logger.info("💾 正在保存 {} 个知识块...", embeddingBatch.size());
            if (!embeddingBatch.isEmpty()) {
                saveToRedis(embeddingBatch, contentBatch);
            }

            // 5. 更新元数据
            if (knowledgeBaseConfig.getUpdateInterval() > 0)
            {
                redisTemplate.opsForValue().set(
                        KB_UPDATE_TIME,
                        LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
                        knowledgeBaseConfig.getUpdateInterval(),
                        TimeUnit.DAYS
                );
            } else
            {
                // 如果更新间隔 小于等于 0，则设置更新时间为 1 小时
                redisTemplate.opsForValue().set(
                        KB_UPDATE_TIME,
                        LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
                        1,
                        TimeUnit.HOURS
                );
            }

            long elapsed = (System.currentTimeMillis() - progress.startTime) / 1000;
            logger.info("✅ 更新完成! 成功处理: {}/{} 项 | 生成块: {} | 总耗时: {}秒",
                    successCount.get(), progress.totalItems,
                    progress.processedChunks, elapsed);

        } catch (Exception e) {
            logger.error("❌ 任务执行失败", e);
        }
    }

    // 加权排序
    private Map<SourceType, List<KnowledgeItem>> weightedSorting(List<KnowledgeItemSimilarity> items, String query) {
        String lcQuery = query.toLowerCase();
        Map<SourceType, List<KnowledgeItem>> result = new EnumMap<>(SourceType.class);
        result.put(SourceType.File, new ArrayList<>(items.size() / 2));
        result.put(SourceType.Table, new ArrayList<>(items.size() / 2));

        // 预处理：计算加权分数
        items.forEach(item -> {
            if (item.similarity > knowledgeBaseConfig.getSimilarityThreshold()) {
                int pos = item.content.getContent().toLowerCase().indexOf(lcQuery);
                if (pos >= 0) {
                    float positionBoost = 1.0f - (Math.min(pos, 100) / 100.0f);
                    item.similarity *= (1 + positionBoost * 0.3f);
                }
            }
        });

        // 排序和分发
        items.stream()
                .filter(item -> item.similarity > knowledgeBaseConfig.getSimilarityThreshold())
                .sorted(Comparator.comparingDouble(item -> -item.similarity))
                .map(item -> item.content)
                .forEach(item -> result.get(item.getSourceType()).add(item));

        return result;
    }

    // 获取前N条结果
    private static List<KnowledgeItem> getTopNResults(List<KnowledgeItem> items, int n) {
        if (items == null || items.isEmpty()) {
            return Collections.emptyList();
        }
        return items.stream().limit(n).collect(Collectors.toList());
    }

    // 清理旧数据
    private void cleanPreviousData() {
        redisTemplate.delete(KB_EMBEDDING_DIR);
        Set<String> keys = redisTemplate.keys(KB_CONTENT_DIR + "*");
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys); // 批量删除
        }
    }

    // 判断文件是否支持
    private boolean isSupportedFile(Path file) {
        String fileName = file.toString().toLowerCase();
        return knowledgeBaseConfig.getSupportedExtensions().stream()
                .anyMatch(ext -> fileName.endsWith(ext.toLowerCase()));
    }

    // 从文件中读取知识数据
    private List<KnowledgeItem> safeReadFiles(String dir) {
        List<KnowledgeItem> items = new ArrayList<>();

        // 1. 优先尝试从 classpath/resources 读取
        List<Path> filePaths = getResourceFiles(dir);

        // 2. 如果 resources 中未找到文件，尝试文件系统路径（相对/绝对）
        if (filePaths.isEmpty()) {
            logger.warn("⚠️ 未找到文件夹:{}，尝试从文件系统读取...", dir);
            Path targetDir = Paths.get(dir).toAbsolutePath().normalize();
            if (!Files.exists(targetDir) || !Files.isDirectory(targetDir)) {
                logger.warn("⚠️ 目录不存在或不是文件夹: {}", targetDir);
                return items;
            }
            try (Stream<Path> files = Files.list(targetDir)) {
                filePaths = files.filter(Files::isRegularFile)
                        .filter(p -> isSupportedFile(p)) // 使用配置的扩展名检查
                        .collect(Collectors.toList());
                logger.info("✅ 找到 {} 个知识文件", filePaths.size());
            } catch (IOException e) {
                logger.error("⚠️ 目录读取失败: {}", targetDir, e);
                return items;
            }
        }
        else
        {
            logger.info("✅ 找到 {} 个知识文件", filePaths.size());
        }

        // 处理所有找到的文件
        filePaths.forEach(file -> processFile(file, items));

        return items;
    }

    // 获取 classpath/resources 下的文件路径（兼容 JAR 包）
    private List<Path> getResourceFiles(String dir) {
        List<Path> paths = new ArrayList<>();
        try {
            // 获取资源 URL（处理 JAR 内路径）
            Enumeration<URL> resources = KnowledgeBaseServiceImpl.class.getClassLoader().getResources(dir);
            while (resources.hasMoreElements()) {
                URL resourceUrl = resources.nextElement();
                if ("file".equals(resourceUrl.getProtocol())) {
                    // 开发环境：直接转换为 Path
                    Path resourcePath = Paths.get(resourceUrl.toURI());
                    if (Files.isDirectory(resourcePath)) {
                        try (Stream<Path> files = Files.list(resourcePath)) {
                            files.filter(Files::isRegularFile)
                                    .filter(p -> isSupportedFile(p)) // 使用配置的扩展名检查
                                    .forEach(paths::add);
                        }
                    }
                } else if ("jar".equals(resourceUrl.getProtocol())) {
                    // 生产环境（JAR 包）：解析 JAR 内部路径
                    String jarPath = resourceUrl.getPath().substring(5, resourceUrl.getPath().indexOf("!"));
                    String resourceDir = resourceUrl.getPath().substring(resourceUrl.getPath().indexOf("!") + 2);
                    try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8.name()))) {
                        Enumeration<JarEntry> entries = jar.entries();
                        while (entries.hasMoreElements()) {
                            String entryName = entries.nextElement().getName();
                            if (entryName.startsWith(resourceDir)) {
                                paths.add(Paths.get(entryName));
                            }
                        }
                    }
                }
            }
        } catch (IOException | URISyntaxException e) {
            logger.debug("资源路径解析失败: {}", dir, e);
        }
        return paths.stream()
                .filter(p -> isSupportedFile(p)) // 使用配置的扩展名检查
                .collect(Collectors.toList());
    }

    // 处理单个文件（兼容 JAR 内文件）
    private void processFile(Path filePath, List<KnowledgeItem> items) {
        try (InputStream is = KnowledgeBaseServiceImpl.class.getClassLoader().getResourceAsStream(filePath.toString())) {
            if (is == null) {
                // 尝试从文件系统直接读取
                if (!Files.exists(filePath)) {
                    logger.error("文件不存在: {}", filePath);
                    return;
                }
                String content = Files.readString(filePath, StandardCharsets.UTF_8);
                items.add(new KnowledgeItem(SourceType.File, content));
            } else {
                // 从 JAR 内读取
                String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
                items.add(new KnowledgeItem(SourceType.File, content));
            }
        } catch (IOException e) {
            logger.error("文件读取失败: {}", filePath, e);
        }
    }

    // 从数据库读取知识数据
    private List<KnowledgeItem> safeReadDatabase() {
        List<KnowledgeItem> items = new ArrayList<>();
        knowledgeDataBase.safeReadDatabase().forEach((key, value) -> {
            items.add(new KnowledgeItem(SourceType.Table, value));
        });
        return items;
    }

    // 批量存储（事务性写入）
    private void saveToRedis(Map<String, float[]> embeddings, Map<String, KnowledgeItem> contents) {
        // 批量保存嵌入向量（自动通过 Jackson 序列化为 JSON）
        redisTemplate.opsForHash().putAll(KB_EMBEDDING_DIR, embeddings);
        // 批量保存内容
        ObjectMapper objectMapper = new ObjectMapper();

        // 然后将序列化后的 JSON 字符串存入 Redis
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            contents.forEach((k, v) -> {
                try {
                    String jsonContent = objectMapper.writeValueAsString(v);
                    connection.stringCommands().set(
                            (KB_CONTENT_DIR + k).getBytes(StandardCharsets.UTF_8),
                            jsonContent.getBytes(StandardCharsets.UTF_8)
                    );
                } catch (JsonProcessingException e) {
                    logger.error("序列化内容失败: {}", v, e);
                }
            });
            return null;
        });
    }

    // 辅助方法：对齐到最近的配置时间点
    private ZonedDateTime alignToScheduledTime(ZonedDateTime time, int scheduledHour) {
        ZonedDateTime alignedTime = time.withHour(scheduledHour)
                .withMinute(0)
                .withSecond(0)
                .withNano(0);
        return time.isAfter(alignedTime) ? alignedTime.plusDays(1) : alignedTime;
    }

    // 知识类型
    enum SourceType {
        File("File"),
        Table("Table");
        private final String type;

        SourceType(String type) {
            this.type = type;
        }

        public String getType() {
            return type;
        }
    }

    // 知识数据
    @Data
    @Accessors(chain = true)
    @NoArgsConstructor // 必须添加无参构造
    private static class KnowledgeItem {
        private SourceType sourceType;
        private String content;

        public KnowledgeItem(SourceType sourceType, String content)
        {
            this.sourceType = sourceType;
            this.content = content;
        }
    }
}
